#include <iostream>
#include <list>
#include <vector>
#define N 100  // Maxima cantidad de nodos que me pueden dar

using namespace std;

// Estructura Union-Find
struct data{
	int name; // Nombre
	int size; // Tamaño
	struct data *home; // Mi padre
};

class makeunionfind { // Clase que realiza el make, el find y el union.
public :
	data S[N]; // Tengo un arreglo de datos. 
public:
	makeunionfind(int n){ // Constructor de la clase, hace el make basicamente para n elementos.
		for(int i=0;i<n;i++) { // Inicializo cada estructura
			S[i].name=i+1; //Inicializo
			S[i].size=1; // y le pongo tamaño 0 a cada uno de los elementos.
			S[i].home=&S[i]; // Pongo mi direccion, yo soy mi padre ahora.
		}
	}
     
	void unionf(int a, int b){ // UNION
		int sizea,sizeb; // Apendeo la estructura de menor tamano a la de mayor, poniendo como su raiz la de mi estructura.
		sizea=S[a-1].home->size; 
		sizeb=S[b-1].home->size;
		if( sizea > sizeb){
			(S[b-1].home)->home=S[a-1].home;
			S[a-1].size=sizea+sizeb; 
		} else {
			(S[a-1].home)->home=S[b-1].home;
			S[b-1].size=sizea+sizeb;
		}
	}

	int find(int a) { //FIND
		data *temp,*temp2,*stoppoint; //Creo tres punteros a estructuras auxiliares
		int result; // Resultado
		temp2=&S[a-1]; //Mi direccion
		temp=S[a-1].home; //Mi home 
		while(temp2!=temp) {                        
			temp2=temp;
			temp=(*temp2).home;  //Busca mi raiz            
		}
		result=temp2->name;
		stoppoint=temp2;
		temp2=&S[a-1];
		do{
			temp=temp2->home;       
			(*temp2).home=stoppoint; //Actualiza bien los caminos compactandola.
			temp2=temp;         
		}while(temp2!=stoppoint);   
		
		return result;                                     
	}
};

typedef int nodo;

class eje{
    public :
		int start, end;
		int cost;
		eje() {//Estructura de eje con constructor por defecto.
			start=0;
			end=0;
			cost=0;
		} 
};

int compare(const void *a,const void *b) {// Comparador de ejes por peso.
	eje *a1,*b1;
	a1 = (eje*)a;
    b1 = (eje*)b;
    if(a1->cost < b1->cost)
    return -1;
    else if (a1->cost > b1->cost)
    return 1;
    else
    return 0;
}

//Algoritmo de kruskal para hallar un arbol generador minimo.
int kruskal(eje *e,int n,int m, list<eje> &sobrantes, eje *ans,vector< list<int> > &adys){
    makeunionfind list(n);
    int suma = 0;
    int (*comp)(const void *a,const void *b );
    int k=0;
    comp=compare;
    // ordena los ejes
    qsort((void*)e,m,sizeof(e[0]),comp);
    int i = 0;
    while( i < m ){
        int s,f;
        // toma los ejes en forma ordenada
        s=e[i].start;
        f=e[i].end;
		
        if(list.find(s)==list.find(f)) { // si ya estaban unidos s y f
			//guardo el eje en sobrantes
            sobrantes.push_back(e[i]); 
        } else { // si no estaban unidos s y f
			list.unionf(s,f);
			ans[k]=e[i];
			// Armo la lista de adyacencia, luego la voy a utilizar.
			adys[s-1].push_back(f); 
			adys[f-1].push_back(s);
			suma += e[i].cost;
			k++;    
        }
		i++;
    }
    return suma;    
}

//Arma la matriz con el valor del eje de peso maximo entre todo par de nodos del primer arbol generador minimo. 
void llenarPesos(int raiz,int padre,int hijo, int maximo, vector< vector<int> > &pesos, vector<list<int> > &adyacencia){
	// hace un dfs, porque tenemos un arbol
    list<int>::iterator pos;
    pos = adyacencia[hijo - 1].begin();
    while( pos != adyacencia[hijo - 1].end()){
        if (*pos != padre){
            maximo = max(maximo, pesos[*pos - 1][hijo - 1]);
            pesos[*pos - 1][raiz - 1] = maximo;
            pesos[raiz - 1][*pos - 1] = maximo;
            llenarPesos(raiz, hijo, *pos, maximo, pesos, adyacencia);
        }
        pos++;
    }
 }

int main() {
    int n,m;
    int casos;
    int S1;
    int S2;
    int fuente;
    int destino;
    cin >> casos;
    while(casos > 0) {
		cin >> n >> m;
		list<int> vacia;
		vector< list<int> > adyacencia(n, vacia); //Lista de adyacencia.
		vector< vector<int> > pesos(n, vector<int>(n,0)); // Estructura que almacena lo que necesita la funcion llenarPesos.
		eje e[m]; // Estructura que guarda todos los ejes.
		list<eje> sob; // Ejes sobrantes del primer arbol.
		for(int i = 0; i < m; ++i){
			cin>> e[i].start;
			cin>> e[i].end;
			cin>> e[i].cost;
		}
        eje ans[n-1]; // Estructura que almacena los ejes del primer arbol generador.
		// LLamada para el calculo de la suma de los ejes del primer arbol.
        S1 = kruskal(e,n,m, sob, ans, adyacencia); 
        cout << S1 << " ";
     
        for(int k= 0; k < n-1; k++ ){
            pesos[(ans[k].end)-1][(ans[k].start)-1] = ans[k].cost;
			pesos[(ans[k].start)-1][(ans[k].end)-1] = ans[k].cost;
        }
    
        S2 = 300000; // Cota maxima.
     
        list<eje>::iterator pos;
        pos = sob.begin();
     
        for (int j = 1; j <= n;j++){
           llenarPesos(j, j , j,  0, pesos, adyacencia);
        }
    
        while( pos != sob.end()){
            fuente = (*pos).start;
			destino = (*pos).end;
         
			int S2min = S1 - pesos[fuente-1][destino-1] + (*pos).cost;
         
			if (S2min < S2) S2 = S2min;
			
			pos++;
        }
     
        cout << S2 << endl;
        casos--;       
    } 
    return 0;
}
